package metaphor.persistence.filter;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

/**
 * A search criteria
 * 
 * <strong>Changes</strong>
 * <ol>
 * <li>Added the search pattern that was in the Jpa Service</li>
 * <li>Changed to work with the DetachedCriteria object</li>
 * <li>Added the order properties</li>
 * <li>Added the persistent fields property so that the HibernateDao did not depend on the model classes</li>
 * <li>Added the association property so that the search can include associated classed</li>
 * </ol>
 * @author Spring Web Flow
 * @author Mark Ashworth <javameme@gmail.com>
 * @version 1.0.0
 */
public class SearchCriteria implements Serializable {
    private static final long serialVersionUID = 1L;
    protected final Logger logger = LoggerFactory.getLogger(getClass());
    /**
     * The persistent class that the search criteria search for
     */
    private Class persistentClass;
    /** 
     * The persistent class fields that should be part of the search 
     */
    private List<String> persistentFields = new ArrayList<String>();
    /**
     * The user-provided search criteria for finding Hotels.
     */
    private String searchString;
    /**
     * The maximum page size of the Hotel result list
     */
    private int pageSize;
    /**
     * The current page of the Hotel result list.
     */
    private int page;
    /**
     * Whether the search criteria is paged
     */
    private boolean paged = false;
    /**
     * The order the search results should be returned by
     */
    private List<Order> orderBy = new ArrayList<Order>();

    /**
     * Associations to the main persistent class. Used for example to get
     * the persons that have an employee no. of A002. For this search to work
     * the search criteria needs to link to the employee association of
     * person to get the employee no.
     */
    private List<Association> associations;

    /**
     * The default constructor. However the persistentClass is required.
     */
    public SearchCriteria() {
        super();
    }

    /**
     * Search criteria for a persistent class
     * @param persistentClass The persistent class instances to search for
     * @param fields The persistent class fields that make up the search
     */
    public SearchCriteria(Class persistentClass, String[] fields) {
        this(persistentClass, null, fields);
    }
    
    /**
     * Search criteria for a persistent class and a search string
     * @param persistentClass The persistent class instances to search for
     * @param searchString The search string
     * @param fields The persistent class fields that make up the search
     */
    public SearchCriteria(Class persistentClass, String searchString, String[] fields) {
        this(persistentClass, searchString, 0, 0, fields);
    }
    
    /**
     * Search criteria for a persistent class and a search string
     * @param persistentClass The persistent class instances to search for
     * @param searchString The search string
     * @param pageSize The number of results to return per page
     * @param page The page number within the results
     * @param fields The persistent class fields that make up the search
     */
    public SearchCriteria(Class persistentClass, String searchString, int pageSize, int page, String...fields) {
        setPersistentClass(persistentClass);
        setSearchString(searchString);
        setPageSize(pageSize);
        setPage(page);
        setPersistentFields(Arrays.asList(fields));
    }
    
    /**
     * The root persistent class for the search
     * @param persistentClass The persistent class
     * @return SearchCriteria
     */
    public static SearchCriteria forClass(Class persistentClass) {
        return new SearchCriteria(persistentClass, "", new String[0]);
    }
    
    /**
     * The root persistent class for the search
     * @param persistentClass The persistent class
     * @param search The search expression
     * @param fields The fields to search in
     * @return SearchCriteria
     */
    public static SearchCriteria forClass(Class persistentClass, String search, String[] fields) {
        return new SearchCriteria(persistentClass, search, fields);
    }
    
    /**
     * Adds the field to the list of field that the search expression will
     * searched in. If the field is on an association then that association
     * alias should be appended to the field name, for example:<br />
     * 
     * searchCriteria.innerJoin("employee", "e");<br />
     * searchCriteria.in("e.employeeNo");<br />
     * 
     * @param field The field name
     * @return SearchCriteria
     */
    public SearchCriteria in(String field) {
        getPersistentFields().add(field);
        return this;
    }

    /**
     * Sets the list of fields that the search criteria will use to find the
     * expression.
     * If the field is on an association then that association
     * alias should be appended to the field name, for example:<br />
     * 
     * searchCriteria.innerJoin("employee", "e");<br />
     * searchCriteria.in("e.employeeNo");<br />
     *
     * @param fields The field names
     * @return SearchCriteria
     */
    public SearchCriteria in(String[] fields) {
        this.setPersistentFields(Arrays.asList(fields));
        return this;
    }

    /**
     * The search expression to look for. This string should be free from an
     * database search characters like % and rather use *, for example<br />
     *
     * m*<br />
     *
     * @param search The search expression
     * @return SearchCriteria
     */
    public SearchCriteria search(String search) {
        this.setSearchString(search);
        return this;
    }

    /**
     * The root persistent class for the search
     * @return Class
     */
    public Class getPersistentClass() {
        return persistentClass;
    }

    public void setPersistentClass(Class persistentClass) {
        this.persistentClass = persistentClass;
    }
    
    /**
     * Used to set the persistentClass from the class name, for example
     * xperiment.metaphor.model.user.User<br />
     * 
     * @param persistentClass The name of the class
     * @throws java.lang.ClassNotFoundException If the class was not found
     */
    public void setPersistentClassName(String persistentClass) throws ClassNotFoundException {
        this.persistentClass = Class.forName(persistentClass);
    }
    
    /**
     * Used to retrieve the persistentClass name so that it can be possible
     * set later. The default Class.toString() returns for example 
     * <i>class xperiment.metaphor.model.user.User</i> which causes a 
     * ClassNotFoundException in Spring Web because of the pre-pended word 
     * <i>class</i>.
     * 
     * @return String
     */
    public String getPersistentClassName() {
        return (this.persistentClass == null ? null : this.persistentClass.getName());
    }

    public List<String> getPersistentFields() {
        if (this.persistentFields == null) {
            this.persistentFields = new ArrayList<String>();
        }
        return persistentFields;
    }

    public void setPersistentFields(List<String> persistentFields) {
        this.persistentFields = persistentFields;
    }
    
    public String getSearchString() {
        return searchString;
    }

    public void setSearchString(String searchString) {
        if (logger.isDebugEnabled()) {logger.debug("SearchCriteria.search=" + searchString);}
                
        this.searchString = searchString;
    }

    public int getPageSize() {
        return pageSize;
    }

    /**
     * Sets the page size of the search results. Setting this value to less than
     * 1 makes the seach criteria non-paged.
     * 
     * @param pageSize The page size of the search results
     * @return SearchCriteria
     */
    public void setPageSize(int pageSize) {
        if (logger.isDebugEnabled()) {logger.debug("SearchCriteria.pageSize=" + pageSize);}
        
        this.pageSize = pageSize;
        this.paged = this.pageSize > 0;
    }

    public int getPage() {
        return page;
    }

    /**
     * Sets the page of the search results
     * @param page The page number of the search results
     * @return SearchCriteria
     */
    public void setPage(int page) {
        if (logger.isDebugEnabled()) {logger.debug("SearchCriteria.page=" + page);}
        
        this.page = page;
    }

    /**
     * Increments the search page
     * @return SearchCriteria
     */
    public SearchCriteria incrementPage() {
        this.page++;
        return this;
    }
    
    public List<Order> getOrderBy() {
        return orderBy;
    }

    /**
     * Adds the list of propertyNames to order by
     * @param orderBy The list of property names
     * @return SearchCriteria
     */
    public void setOrderBy(List<Order> orderBy) {
        this.orderBy = orderBy;
    }

    public boolean isPaged() {
        return paged;
    }

    public void setPaged(boolean paged) {
        if (logger.isDebugEnabled()) {logger.debug("SearchCriteria.paged=" + paged);}
        this.paged = paged;
    }
    
    /**
     * Returns the search pattern to be used by the database for the specified
     * search string.
     * @return String
     */
    public String getSearchPattern() {
        
        String pattern = "%";
        
        if (StringUtils.hasText(searchString)) {
            String search = new String(searchString);
            pattern = "%" + search.toLowerCase().replace('*', '%') + "%";
        }
        
        if (logger.isDebugEnabled()) {logger.debug("SearchCriteria.pattern=" + pattern);}
        return pattern;
    }
    
    public List<Association> getAssociations() {
        if (this.associations == null) {
            this.associations = new ArrayList<Association>();
        }
		return this.associations;
	}
    
    /**
     * Sets the page size of the searched results
     * @param pageSize The new page size
     * @return SearchCriteria
     */
    public SearchCriteria pageSize(int pageSize) {
        this.setPageSize(pageSize);
        return this;
    }
    
    /**
     * Sets the page of the results
     * @param page The new page
     * @return SearchCriteria
     */
    public SearchCriteria page(int page) {
        this.setPage(page);
        return this;
    }
    
    /**
     * Sets the paging on. Paging is also automatically switched on
     * calls to setPageSize() or pageSize(). 
     * @return SearchCriteria
     */
    public SearchCriteria pagingOn() {
        this.setPaged(true);
        return this;
    }
    
    /**
     * Sets the paging off. Note that paging is automatically switched on
     * calls to setPageSize() or pageSize(). 
     * @return SearchCriteria
     */
    public SearchCriteria pagingOff() {
        this.setPaged(false);
        return this;
    }
    
    /**
     * Adds the property name to the order by list
     * @param propertyName The property name to sort by ascending
     * @return SearchCriteria
     */
    public SearchCriteria asc(String propertyName) {
        this.orderBy.add(Order.asc(propertyName));
        return this;
    }
    
    /**
     * Adds the property name to the order by list
     * @param propertyName The property name to sort by descending
     * @return SearchCriteria
     */
    public SearchCriteria desc(String propertyName) {
        this.orderBy.add(Order.desc(propertyName));
        return this;
    }
    
    /**
     * Creates an inner join association between the forClass object
     * on the path entity with an alias. For example in Hibernate
     * the method would be used like<br />
     * Query.forClass(Person.class).innerJoin("role", "r")<br />
     *
     * @param path The path
     * @param alias The alias
     * @return SearchCriteria
     */
    public SearchCriteria innerJoin(String path, String alias) {
        if (this.associations == null) {
            this.associations = new ArrayList<Association>();
        }
        this.associations.add(new Association(path, alias, JoinType.INNER_JOIN));
        return this;
    }

    public SearchCriteria fullJoin(String path, String alias) {
        if (this.associations == null) {
            this.associations = new ArrayList<Association>();
        }
        this.associations.add(new Association(path, alias, JoinType.FULL_JOIN));
        return this;
    }

    public SearchCriteria leftOuterJoin(String path, String alias) {
        if (this.associations == null) {
            this.associations = new ArrayList<Association>();
        }
        this.associations.add(new Association(path, alias, JoinType.LEFT_OUTER_JOIN));
        return this;
    }

    public SearchCriteria rightOuterJoin(String path, String alias) {
        if (this.associations == null) {
            this.associations = new ArrayList<Association>();
        }
        this.associations.add(new Association(path, alias, JoinType.RIGHT_OUTER_JOIN));
        return this;
    }
}